﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Net.Mail;

namespace ExchangeHog.MessageParser
{
	class TheParser
	{
		Stack<string> boundaryDelims;
		Stream s;
		StreamReader sr;

		public TheParser(Stream s) { this.s = s; sr = new StreamReader(s, Encoding.ASCII); }

		Queue<string> lines;
		string ReadLine()
		{
			if (lines.Count > 0)
				return lines.Dequeue();
			else
				return sr.ReadLine();
		}
		void PutLineBack(string s)
		{
			lines.Enqueue(s);
		}
		//
		//	Keep reading lines while they end with backslash and next starts with whitespace
		//
		string ReadAndUnfoldOneLine()
		{
			string str = ReadLine();
			while (str.EndsWith("\\"))
			{
				string nextline = ReadLine();
				if (nextline.Length > 0 && (nextline[0] == ' ' || nextline[0] == '\t'))
					str = String.Concat(str, nextline);
				else
					break;
			}
			return str.Trim(' ', '\t');
		}

		string ReadMessageHeaderContents()
		{
			StringBuilder sb = new StringBuilder();
			while (true)
			{
				string str = ReadAndUnfoldOneLine();
				if (str.Length == 0)
					break;
				sb.Append(str);
			}
			return sb.ToString();
		} // end of ReadMessageHeaderContents

		string StripComments(string s)
		{
			if (!s.Contains('('))
				return s;
			else
			{
				StringBuilder sb = new StringBuilder();
				int skippingCommentsDepth = 0;
				for (int i=0; i<s.Length; i++)
					switch(s[i])
					{
						case '(': skippingCommentsDepth++; break;
						case ')': skippingCommentsDepth--; break;
						default:
							if (skippingCommentsDepth == 0)
								sb.Append(s[i]);
							break;
					}
				return sb.ToString();
			}
		} // end of StripComments

		void SkipWhitespace(string s, ref int ndx)
		{
			while (ndx < s.Length && (s[ndx] == ' ' || s[ndx] == '\t')) ndx++;
		}


		void ReadMessageHeaders(ref MailMessage mm)
		{
			string str = ReadAndUnfoldOneLine();

			//
			//	Headers
			//
			int ndxColon = str.IndexOf(':');
			if (ndxColon >= 0)
			{
				string nameHeader = str.Substring(0, ndxColon - 1).Trim(' ', '\t');
				string valueHeader = (ndxColon < str.Length - 1)? str.Substring(ndxColon + 1).Trim(' ', '\t'): "";
				switch (nameHeader)
				{
					case "From":		mm.From = ParseAddress(valueHeader); break;
					case "Sender":		mm.Sender = ParseAddress(valueHeader); break;
					case "Reply-To":	foreach (System.Net.Mail.MailAddress maReplyTo in ParseAddresses(valueHeader)) mm.ReplyToList.Add(maReplyTo); break;
					case "To":			foreach (System.Net.Mail.MailAddress maTo in ParseAddresses(valueHeader)) mm.To.Add(maTo); break;
					case "cc":			foreach (System.Net.Mail.MailAddress maCC in ParseAddresses(valueHeader)) mm.CC.Add(maCC); break;
					case "bcc":			foreach (System.Net.Mail.MailAddress maBCC in ParseAddresses(valueHeader)) mm.Bcc.Add(maBCC); break;
					case "Subject":		mm.Subject = ParseText(s); break;

					//
					//	from RFC 2045
					//
					case "MIME-Version":
						//
						//	parse text ignoring comments in parenthesis
						//
						string valuewithoutcomments = StripComments(valueHeader);
						if (valuewithoutcomments != "1.0")
							throw new ApplicationException("Expected 1.0 as a value for MIME-Version header. Instead got " + valuewithoutcomments);
						break;
					case "Content-Type":
						mm.Headers.Add(nameHeader, valueHeader);
						//
						//	content := "Content-Type" ":" type "/" subtype *(";" parameter) ; Matching of media type and subtype is ALWAYS case-insensitive.
						//	type := "text" / "image" / "audio" / "video" / "application" / extension-token / "message" / "multipart" / extension-token
						//	parameter := token "=" (token / quoted-string)
						//	token := 1*<any (US-ASCII) CHAR except SPACE, CTLs, or tspecials>
						//	tspecials :=  "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "\" / <"> / "/" / "[" / "]" / "?" / "=" ; Must be in quoted-string, to use within parameter values
						//
						if (valueHeader[0] != '\"') throw new ApplicationException(String.Format("Unexpected {0}, expected '{1}'", valueHeader[0], "\""));
						int ndx = 1;
						if (ndx >= valueHeader.Length) throw new ApplicationException(String.Format("Invalid quoted string '{0}'", valueHeader));
						StringBuilder sb = new StringBuilder();
						while (ndx < valueHeader.Length && valueHeader[ndx] != '\"')
							sb.Append(valueHeader[ndx++]);
						if (ndx >= valueHeader.Length) throw new ApplicationException(String.Format("Invalid quoted string '{0}'", valueHeader));
						ndx++;
						string contenttype = sb.ToString();

						SkipWhitespace(valueHeader, ref ndx);
						if (ndx >= valueHeader.Length) throw new ApplicationException(String.Format("Invalid Content-Type value: no slash, no subtype '{0}'", valueHeader));
						if (valueHeader[ndx] != '/') throw new ApplicationException(String.Format("Unexpected {0}, expected '{1}'", valueHeader[0], "/"));
						ndx++;
						SkipWhitespace(valueHeader, ref ndx);
						if (ndx >= valueHeader.Length) throw new ApplicationException(String.Format("Invalid Content-Type value: no subtype '{0}'", valueHeader));

						if (valueHeader[ndx] != '\"') throw new ApplicationException(String.Format("Unexpected {0}, expected '{1}'", valueHeader[0], "\""));
						ndx++;
						if (ndx >= valueHeader.Length) throw new ApplicationException(String.Format("Invalid quoted string '{0}'", valueHeader));
						sb.Clear();
						while (ndx < valueHeader.Length && valueHeader[ndx] != '\"')
							sb.Append(valueHeader[ndx++]);
						if (ndx >= valueHeader.Length) throw new ApplicationException(String.Format("Invalid quoted string '{0}'", valueHeader));
						string contentsubtype = sb.ToString();

						while (ndx < valueHeader.Length)
						{
							SkipWhitespace(valueHeader, ref ndx);
							if (ndx < valueHeader.Length && valueHeader[ndx] == ';')
							{
								ndx++;
								sb.Clear();
								bool goodTokenChar = true;
								while (ndx < valueHeader.Length && goodTokenChar)
								{
									goodTokenChar = false;
									switch (valueHeader[ndx])
									{
										case ' ':case '\t':
										case '(':case ')':case '<':case '>':case '@':case ',':case ';':case ':':case '\\':case '\"':case '/':case '[':case ']':case '?':case '=': break;
										default:
											if (((int)valueHeader[ndx]) > 32 && ((int)valueHeader[ndx]) < 127)
											{
												sb.Append(valueHeader[ndx++]);
												goodTokenChar = true;
											}
									}
								}
								string nameParam = sb.ToString();
								SkipWhitespace(valueHeader, ref ndx);

								if (ndx >= valueHeader.Length) throw new ApplicationException(String.Format("Invalid Content-Type parameter {0}: no value specified", valueHeader));
								if (valueHeader[ndx] != '=') throw new ApplicationException(String.Format("Unexpected {0}, expected '{1}'", valueHeader[0], "="));
								ndx++;
								SkipWhitespace(valueHeader, ref ndx);
								if (ndx >= valueHeader.Length) throw new ApplicationException(String.Format("Invalid Content-Type parameter {0}: no value specified", valueHeader));

								sb.Clear();
								if (valueHeader[ndx] == '\"')
								{
									ndx++;
									do
									{
										if (valueHeader[ndx] == '\\')
										{
											ndx++;
											if (ndx >= valueHeader.Length)
											sb.Append(valueHeader[ndx]);
									}
								}
								else
								{
									goodTokenChar = true;
									while (ndx < valueHeader.Length && goodTokenChar)
									{
										goodTokenChar = false;
										switch (valueHeader[ndx])
										{
											case ' ':case '\t':
											case '(':case ')':case '<':case '>':case '@':case ',':case ';':case ':':case '\\':case '\"':case '/':case '[':case ']':case '?':case '=': break;
											default:
												if (((int)valueHeader[ndx]) > 32 && ((int)valueHeader[ndx]) < 127)
												{
													sb.Append(valueHeader[ndx++]);
													goodTokenChar = true;
												}
										}
									}
								}
								string valueParam = sb.ToString();
							}
						}
						break;
				case "Conent-Transfer-Encoding":
					//
					//	encoding := "Content-Transfer-Encoding" ":" mechanism
					//	
					//	mechanism := "7bit" / "8bit" / "binary" /
					//	             "quoted-printable" / "base64" /
					//	             ietf-token / x-token
					//
					cte = ParseHeaderContentAsIs(s);
					mm.Headers.Add(headerName, cte);
					break;
				case "Content-ID":
					//
					//	id := "Content-ID" ":" msg-id
					//
					mm.Headers.Add(headerName, ParseHeaderContentAsIs(s));
					break;
				case "Content-Description":
					//
					//	description := "Content-Description" ":" *text
					//
					mm.Headers.Add(headerName, ParseHeaderContentAsIs(s));
					break;

				case "Content-Disposition":
					//
					//	from RFC 2183
					//
					//	disposition := "Content-Disposition" ":"
					//	               disposition-type
					//	               *(";" disposition-parm)
					//	
					//	disposition-type := "inline"
					//	                  / "attachment"
					//	                  / extension-token
					//	                  ; values are not case-sensitive
					//	
					//	disposition-parm := filename-parm
					//	                  / creation-date-parm
					//	                  / modification-date-parm
					//	                  / read-date-parm
					//	                  / size-parm
					//	                  / parameter
					//	
					//	filename-parm := "filename" "=" value
					//	
					//	creation-date-parm := "creation-date" "=" quoted-date-time
					//	
					//	modification-date-parm := "modification-date" "=" quoted-date-time
					//	
					//	read-date-parm := "read-date" "=" quoted-date-time
					//	
					//	size-parm := "size" "=" 1*DIGIT
					//	
					//	quoted-date-time := quoted-string
					//	                 ; contents MUST be an RFC 822 `date-time'
					//	                 ; numeric timezones (+HHMM or -HHMM) MUST be used
					//
					mm.Headers.Add(headerName, ParseContentDisposition(s, out cd));
					break;
					case "Date":
					case "Resent-Date":
					case "Return-path":
					case "Resent-From":
					case "Resent-Sender":
					case "Resent-Reply-To":  
					case "Resent-To":
					case "Resent-cc":
					case "Resent-bcc":
					default:
						mm.Headers.Add(nameHeader, valueHeader); break;
					break;
			}
		}
		
		MailMessage BuildMessage(Stream s)
		{
			MailMessage mm = new MailMessage();

			ReadMessageHeaders();



			return mm;
		}
	}
}
